package com.usbserial.scale;

import com.usbserial.exception.ParseException;

import java.nio.charset.StandardCharsets;

/**
 * Parses raw payloads emitted by supported scale protocols into {@link ScaleData} records.
 */
public class ScaleProtocolParser {

    private final Protocol protocol;

    public ScaleProtocolParser(Protocol protocol) {
        this.protocol = protocol;
    }

    public ScaleData parse(byte[] rawData) throws ParseException {
        if (rawData == null || rawData.length == 0) {
            throw new ParseException("Empty scale payload");
        }
        switch (protocol) {
            case TOLEDO:
                return parseToledo(rawData);
            case METTLER_TOLEDO:
                return parseMettlerToledo(rawData);
            case GENERIC_HID:
                return parseGenericHid(rawData);
            case AUTO_DETECT:
                return autoDetect(rawData);
            default:
                throw new ParseException("Unsupported protocol: " + protocol);
        }
    }

    private ScaleData parseToledo(byte[] data) throws ParseException {
        // Example format: <STX>+<sign><digits><space><unit><ETX>
        if (data.length < 7) {
            throw new ParseException("Invalid Toledo frame length");
        }
        if (data[0] != 0x02 || data[data.length - 1] != 0x03) {
            throw new ParseException("Missing Toledo sentinels");
        }
        String payload = new String(data, 1, data.length - 2, StandardCharsets.US_ASCII).trim();
        return parseSignedPayload(payload);
    }

    private ScaleData parseMettlerToledo(byte[] data) throws ParseException {
        // Simplified parser for demonstration: expect ASCII "ST,<weight>,<unit>"
        String payload = new String(data, StandardCharsets.US_ASCII).trim();
        String[] parts = payload.split(",");
        if (parts.length < 3) {
            throw new ParseException("Invalid Mettler Toledo payload: " + payload);
        }
        if (!parts[0].equalsIgnoreCase("ST")) {
            throw new ParseException("Scale not stable: " + parts[0]);
        }
        double value = parseDouble(parts[1]);
        ScaleData.WeightUnit unit = parseUnit(parts[2]);
        return new ScaleData(value, unit, ScaleData.ScaleStatus.STABLE);
    }

    private ScaleData parseGenericHid(byte[] data) throws ParseException {
        // USB HID POS scale reports use 6-byte packets.
        if (data.length < 6) {
            throw new ParseException("Invalid HID payload length");
        }
        int unitCode = data[1] & 0xFF;
        ScaleData.WeightUnit unit = (unitCode == 0x02) ? ScaleData.WeightUnit.POUND : ScaleData.WeightUnit.GRAM;
        // Weight stored little endian in bytes 4+5.
        int rawWeight = ((data[4] & 0xFF) << 8) | (data[5] & 0xFF);
        double value = rawWeight / 10.0;
        return new ScaleData(value, unit, ScaleData.ScaleStatus.STABLE);
    }

    private ScaleData autoDetect(byte[] data) throws ParseException {
        if (data.length >= 6 && data[0] == 0x25) {
            return parseToledo(data);
        }
        if (data.length >= 6 && (data[0] & 0xFF) == 0x02) {
            return parseToledo(data);
        }
        if (data.length >= 6) {
            return parseGenericHid(data);
        }
        throw new ParseException("Unable to auto-detect protocol");
    }

    private ScaleData parseSignedPayload(String payload) throws ParseException {
        String[] parts = payload.split(" ");
        if (parts.length < 2) {
            throw new ParseException("Unexpected payload: " + payload);
        }
        double value = parseDouble(parts[0]);
        ScaleData.WeightUnit unit = parseUnit(parts[1]);
        ScaleData.ScaleStatus status = value == 0 ? ScaleData.ScaleStatus.ZERO : ScaleData.ScaleStatus.STABLE;
        return new ScaleData(value, unit, status);
    }

    private double parseDouble(String source) throws ParseException {
        try {
            return Double.parseDouble(source);
        } catch (NumberFormatException ex) {
            throw new ParseException("Invalid weight value: " + source, ex);
        }
    }

    private ScaleData.WeightUnit parseUnit(String token) {
        String normalized = token.trim().toUpperCase();
        switch (normalized) {
            case "G":
            case "GRAM":
                return ScaleData.WeightUnit.GRAM;
            case "KG":
            case "KILOGRAM":
                return ScaleData.WeightUnit.KILOGRAM;
            case "LB":
            case "LBS":
                return ScaleData.WeightUnit.POUND;
            case "OZ":
                return ScaleData.WeightUnit.OUNCE;
            default:
                return ScaleData.WeightUnit.GRAM;
        }
    }

    public enum Protocol {
        TOLEDO,
        METTLER_TOLEDO,
        GENERIC_HID,
        AUTO_DETECT
    }
}
